// $Id: input_xpt.cpp 2 2009-09-02 08:47:42Z joseph2002 $
/*
 * Copyright 2009 Joseph Wu
 * Distributed under the Boost Software License, Version 1.0.
 * (See accompanying file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)
 *
 * For more information, see http://xpcomplus.sourceforge.net
 */

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <new>
#include <boost/assert.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/lexical_cast.hpp>
#include <xpgen/input_xpt.hpp>
#include <xpgen/model.hpp>
#include "xpt_xdr.h"
#include "names.hpp"

using namespace std;
namespace fs = boost::filesystem;

namespace xpgen {

input_xpt::input_xpt() :
    guess_notxpcom_result(false),
    arena_(0)
{
    arena_ = XPT_NewArena(1024, sizeof(double), "main input_xpt arena");
    if (!arena_)
        throw bad_alloc();
}

input_xpt::~input_xpt()
{
    XPT_DestroyArena(arena_);
}

void input_xpt::do_reset()
{
    input::do_reset();

    itf_map_.clear();
    tmp_refs_.clear();
}

void input_xpt::do_module()
{
    // Check input file
    size_t in_size = static_cast<size_t>(fs::file_size(input_file));
    if (in_size == 0) {
        throw runtime_error(
                "file (" + input_file.file_string() + ") is empty");
    }

    // Read input file
    if (verbose)
        cout << ">>> Reading XPT file (" << input_file.leaf() << ")..." << endl;

    fs::ifstream f(input_file, ios_base::in | ios_base::binary);
    vector<char> buf(in_size);

    if (!f.read(&buf[0], in_size)) {
        throw runtime_error("failed to read input file");
    }

    f.close();

    if (verbose)
        cout << "<<< End reading XPT file (" << input_file.leaf() << ")." << endl;

    // Process the xpt
    if (verbose)
        cout << ">>> Processing XPT file (" << input_file.leaf() << ")..." << endl;

    XPTState* state = 0;
    state = XPT_NewXDRState(XPT_DECODE, &buf[0], in_size);
    if (!state) {
        throw runtime_error("failed to make XPT decoder");
    }

    try {
        XPTCursor cursor;
        XPTHeader* header = 0;

        if (!XPT_MakeCursor(state, XPT_HEADER, 0, &cursor)) {
            throw runtime_error("failed to make XPT decoder");
        }

        if (!XPT_DoHeader(arena_, &cursor, &header)) {
            throw runtime_error("invalid XPT file");
        }

        BOOST_ASSERT(header);
        do_header(header);

    } catch (...) {
        XPT_DestroyXDRState(state);
        throw;
    }

    XPT_DestroyXDRState(state);
    if (verbose)
        cout << ">>> End processing XPT file (" << input_file.leaf() << ")." << endl;
}

void input_xpt::do_header(XPTHeader* header)
{
    vector<interface_decl*>& itfs = module()->interfaces;

    BOOST_ASSERT(itfs.empty());
    itfs.reserve(header->num_interfaces + 1);
    itf_map_.reserve(header->num_interfaces + 1);

    // Make root interface
    // XXX I know it may be not exception safe, but to make things simpler...
    // and |reserve| call before cause push_back do not throw?
    // Same for other allocations
    interface_decl* root = new interface_decl(module());
    itfs.push_back(root);

    init_root_interface(root);
    itf_map_.push_back(root);

    // Make all interfaces
    for (int i = 0; i < header->num_interfaces; ++i) {
        XPTInterfaceDirectoryEntry* ide = &header->interface_directory[i];
        check_interface(ide);

        if (is_root_interface(ide)) {
            itf_map_.push_back(root);
        } else {
            interface_decl* itf = new interface_decl(module());
            itfs.push_back(itf);

            init_interface(itf, ide);
            itf_map_.push_back(itf);
        }
    }

    // Setup interfaces, for base, ...
    for (size_t i = 1; i < itfs.size(); ++i) {
        setup_interface(itfs[i]);
    }

    for (size_t i = 1; i < itfs.size(); ++i) {
        if (itfs[i]->user_data) // resolved
            do_interface(itfs[i]);
    }
}

void input_xpt::do_interface(interface_decl* itf)
{
    BOOST_ASSERT(tmp_refs_.empty());

    XPTInterfaceDescriptor* desc =
        static_cast<XPTInterfaceDescriptor*>(itf->user_data);
    BOOST_ASSERT(desc);

    // Constants
    itf->constants.reserve(desc->num_constants);
    for (int i = 0; i < desc->num_constants; ++i) {
        XPTConstDescriptor* cd = &desc->const_descriptors[i];
        check_constant(itf, cd);

        constant_decl* constant = new constant_decl(itf);
        itf->constants.push_back(constant);

        init_constant(constant, cd);
    }

    // Methods
    itf->methods.reserve(desc->num_methods);
    for (int i = 0; i < desc->num_methods; ++i) {
        XPTMethodDescriptor* md = &desc->method_descriptors[i];
        check_method(itf, md);

        method_decl* method = new method_decl(itf);
        itf->methods.push_back(method);

        init_method(method, md);
    }

    // Set references
    itf->references.assign(tmp_refs_.begin(), tmp_refs_.end());
    tmp_refs_.clear();
}

void input_xpt::init_root_interface(interface_decl* itf)
{
    static const uuid iid =
            { 0x00000000, 0x0000, 0x0000,
                { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } };

    itf->set_name(N_interface, N_xpcom);
    itf->set_iid(iid);

    itf->flags = interface_decl::flag_root | interface_decl::flag_resolved;
    itf->user_data = 0;
}

void input_xpt::init_interface(interface_decl* itf, XPTInterfaceDirectoryEntry* ide)
{
    string ns;

    if (!ns_override.empty())
        ns = ns_override;
    else if (ide->name_space)
        ns = ide->name_space;

    itf->set_name(ide->name, ns);
    itf->set_iid(*reinterpret_cast<const uuid*>(&ide->iid));

    itf->flags = 0;
    XPTInterfaceDescriptor* desc = ide->interface_descriptor;
    if (desc) {
        itf->flags |= interface_decl::flag_resolved;

        if (XPT_ID_IS_SCRIPTABLE(desc->flags))
            itf->flags |= interface_decl::flag_scriptable;

        if (XPT_ID_IS_FUNCTION(desc->flags))
            itf->flags |= interface_decl::flag_function;
    }
    // Store interface descriptor to user data
    itf->user_data = desc;

    // FIXME currently, all uint32 param type in these two interface are size_t, so....
    if (!ide->name_space || !(*ide->name_space)) {
        if (strcmp(ide->name, N_nsIMemory) == 0)
            itf->flags |= flag_check_size_t;
        else if (strcmp(ide->name, N_nsIRecyclingAllocator) == 0)
            itf->flags |= flag_check_size_t;
    }
}

void input_xpt::init_constant(constant_decl* constant, XPTConstDescriptor* cd)
{
    constant->name = cd->name;
    init_type(constant->itf, &constant->type, &cd->type);
    init_constant_value(&constant->type, &constant->value, &cd->value);

    constant->user_data = cd;
}

void input_xpt::init_method(method_decl* method, XPTMethodDescriptor* md)
{
    method->name = md->name;

    method->flags = 0;
    if (XPT_MD_IS_HIDDEN(md->flags))
        method->flags |= method_decl::flag_hidden;
    if (XPT_MD_IS_NOTXPCOM(md->flags))
        method->flags |= method_decl::flag_notxpcom;
    if (XPT_MD_IS_GETTER(md->flags))
        method->flags |= method_decl::flag_getter;
    if (XPT_MD_IS_SETTER(md->flags))
        method->flags |= method_decl::flag_setter;
    if (XPT_MD_IS_CTOR(md->flags))
        method->flags |= method_decl::flag_ctor;

    method->user_data = md;

    // Attribute
    if (method->is_attribute()) {
        XPTParamDescriptor* pd = md->params; // always the first param
        check_param(method->itf, pd);

        // XXX Mark [hidden] if param has type of uuid, since xpt removes [notxpcom]
        // on attributes
        bool hidden = false;

        if (method->is_getter()) {
            init_param(method->result, pd, N_PARAM_attr);
            // Ensure the result has retval flag
            method->result->flags |= param_decl::flag_retval;

            hidden = method->result->type.tag == type_decl::tag_uuid;

        } else {
            init_void_retval_param(method->result);

            param_decl* param = new param_decl(method);
            method->params.push_back(param);

            init_param(param, pd, N_PARAM_attr);

            hidden = param->type.tag == type_decl::tag_uuid;
        }

        if (hidden)
            method->flags |= method_decl::flag_hidden;

        return;
    }

    // Operation
    int num_args = md->num_args;

    if (method->is_notxpcom()) {
        XPTParamDescriptor* pd = md->result;
        check_param(method->itf, pd);

        init_param(method->result, pd, N_PARAM_retval);
        // Ensure the result has retval flag
        method->result->flags |= param_decl::flag_retval;

    } else {
        XPTParamDescriptor* pd_retval = 0;
        if (num_args > 0) {
            XPTParamDescriptor* pd = &md->params[num_args - 1];
            check_param(method->itf, pd);
            if (XPT_PD_IS_RETVAL(pd->flags)) {
                pd_retval = pd;
                --num_args;
            }
        }

        if (pd_retval)
            init_param(method->result, pd_retval, N_PARAM_retval);
        else
            init_void_retval_param(method->result);
    }

    method->params.reserve(num_args);
    for (int i = 0; i < num_args; ++i) {
        string name = N_PARAM_arg;
        name += boost::lexical_cast<string>(i + 1);

        XPTParamDescriptor* pd = &md->params[i];
        check_param(method->itf, pd);

        param_decl* param = new param_decl(method);
        method->params.push_back(param);

        init_param(param, pd, name);
    }

    // Setup param relations
    for (size_t i = 0; i < method->params.size(); ++i) {
        setup_param(method->params[i]);
    }
    if (method->is_result_retval_param()) {
        setup_param(method->result);
    }

    // For [notxpcom], guess return value of nsresult
    if (guess_notxpcom_result && method->is_notxpcom() &&
        method->result->type.tag == type_decl::tag_uint32 &&
        method->result->is_normal())
    {
        for (size_t i = 0; i < method->params.size(); ++i) {
            param_decl* param = method->params[i];
            if (param->type.tag == type_decl::tag_uuid) {
                BOOST_ASSERT(!param->type.is_array);

                // Bingo
                init_void_retval_param(method->result);
                method->flags &= ~method_decl::flag_notxpcom;
                method->flags |= method_decl::flag_hidden; // not scriptable
                break;
            }
        }
    }
}

void input_xpt::init_param(
        param_decl* param, XPTParamDescriptor* pd, const string& name)
{
    param->name = name;
    param->flags = 0;

    if (XPT_PD_IS_SHARED(pd->flags))
        param->flags |= param_decl::flag_shared;
//    if (XPT_PD_IS_CONST(pd->flags))
//        param->flags |= param_decl::flag_const;
    if (XPT_PD_IS_OPTIONAL(pd->flags))
        param->flags |= param_decl::flag_optional;

    if (XPT_PD_IS_IN(pd->flags))
        param->flags |= param_decl::flag_in;
    if (XPT_PD_IS_OUT(pd->flags))
        param->flags |= param_decl::flag_out;
    if (XPT_PD_IS_RETVAL(pd->flags))
        param->flags |= param_decl::flag_retval;

    if (XPT_PD_IS_DIPPER(pd->flags)) {
        BOOST_ASSERT(XPT_PD_IS_IN(pd->flags));
        param->flags &= ~param_decl::flag_in;
        param->flags |= param_decl::flag_out;
    }

    init_type(param->method->itf, &param->type, &pd->type);

    param->user_data = pd;
}

void input_xpt::init_void_retval_param(param_decl* param)
{
    param->name.clear();
    param->flags = param_decl::flag_retval;
    param->type.tag = type_decl::tag_void;
    param->type.is_array = false;
}

void input_xpt::init_type(interface_decl* itf, type_decl* type, XPTTypeDescriptor* td)
{
    static const boost::uint16_t type_map[] = {
            type_decl::tag_int8,            // TD_INT8
            type_decl::tag_int16,           // TD_INT16
            type_decl::tag_int32,           // TD_INT32
            type_decl::tag_int64,           // TD_INT64
            type_decl::tag_uint8,           // TD_UINT8
            type_decl::tag_uint16,          // TD_UINT16
            type_decl::tag_uint32,          // TD_UINT32
            type_decl::tag_uint64,          // TD_UINT64
            type_decl::tag_float,           // TD_FLOAT
            type_decl::tag_double,          // TD_DOUBLE
            type_decl::tag_bool,            // TD_BOOL
            type_decl::tag_char,            // TD_CHAR
            type_decl::tag_wchar,           // TD_WCHAR
            type_decl::tag_void,            // >>>>> TD_VOID
            type_decl::tag_uuid,            // >>>>> TD_PNSIID
            type_decl::tag_domstring,       // >> TD_DOMSTRING
            type_decl::tag_pstring,         // TD_PSTRING
            type_decl::tag_pwstring,        // TD_PWSTRING
            type_decl::tag_interface,       // >> TD_INTERFACE_TYPE
            type_decl::tag_interface_qi,    // TD_INTERFACE_IS_TYPE
            0,                              // >>>>> TD_ARRAY
            type_decl::tag_pstring,         // TD_PSTRING_SIZE_IS
            type_decl::tag_pwstring,        // TD_PWSTRING_SIZE_IS
            type_decl::tag_utf8string,      // >> TD_UTF8STRING
            type_decl::tag_string,          // >> TD_CSTRING
            type_decl::tag_wstring            // >> TD_ASTRING
    };

    int tag = XPT_TDP_TAG(td->prefix);
    if (tag == TD_ARRAY) {
        type->is_array = true;
        XPTInterfaceDescriptor* desc =
                static_cast<XPTInterfaceDescriptor*>(itf->user_data);
        td = &desc->additional_types[td->type.additional_type];
        tag = XPT_TDP_TAG(td->prefix);
    } else {
        type->is_array = false;
    }

    type->tag = type_map[tag];
    type->itf = 0;

    // FIXME hack for size_t
    if (type->tag == type_decl::tag_uint32 && is_interface_need_size_t(itf)) {
        type->tag = type_decl::tag_size;
        return;
    }

    // TD_*STRING
    if (type->is_string()) {
        if (XPT_TDP_IS_POINTER(td->prefix.flags) &&
            !XPT_TDP_IS_REFERENCE(td->prefix.flags))
        {
            type->tag += 4; // XXX tag_string_ptr - tag_string == 4
        }
        return;
    }

    // TD_INTERFACE
    if (tag == TD_INTERFACE_TYPE) {
        type->itf = xpt_interface(td->type.iface);
        // Add to reference interfaces
        if (!type->itf->is_root() && type->itf != itf &&
            !type->itf->is_ancestor_of(itf))
        {
            tmp_refs_.insert(type->itf);
        }
        return;
    }

    // TD_VOID
    if (tag == TD_VOID) {
        if (XPT_TDP_IS_POINTER(td->prefix.flags))
            type->tag = type_decl::tag_native;
        if (XPT_TDP_IS_REFERENCE(td->prefix.flags))
            BOOST_ASSERT(false && "reference to void pointer?");
        return;
    }

    // TD_PNSIID
    if (tag == TD_PNSIID) {
        if (XPT_TDP_IS_REFERENCE(td->prefix.flags)) {
            type->tag = type_decl::tag_uuid_ref;
        } else if (XPT_TDP_IS_POINTER(td->prefix.flags)) {
            type->tag = type_decl::tag_uuid_ptr;
        }
        return;
    }
}

void input_xpt::init_constant_value(
        type_decl* type, constant_value* value, XPTConstValue* cv)
{
    switch (type->tag) {
        case type_decl::tag_int16:
            value->i16 = cv->i16;
            break;
        case type_decl::tag_int32:
            value->i32 = cv->i32;
            break;
        case type_decl::tag_uint16:
            value->ui16 = cv->ui16;
            break;
        case type_decl::tag_uint32:
            value->ui32 = cv->ui32;
            break;

        default:
            BOOST_ASSERT(false && "bad constant type");
    }
}

void input_xpt::setup_interface(interface_decl* itf)
{
    XPTInterfaceDescriptor* desc =
        static_cast<XPTInterfaceDescriptor*>(itf->user_data);

    if (!desc) {
        itf->base = 0; // unresolved
    } else {
        if (desc->parent_interface) {
            itf->base = xpt_interface(desc->parent_interface);
            if (itf->base == itf)
                throw runtime_error("interface " + itf->qname + " has self as base");
        } else
            itf->base = root_interface();
    }
}

void input_xpt::setup_param(param_decl* param)
{
    XPTParamDescriptor* pd =
        static_cast<XPTParamDescriptor*>(param->user_data);

    XPTTypeDescriptor* td = &pd->type;
    int tag = XPT_TDP_TAG(td->prefix);

    // Check if this param is set to sub
    if (tag == TD_INTERFACE_IS_TYPE || tag == TD_ARRAY ||
        tag == TD_PSTRING_SIZE_IS || tag == TD_PWSTRING_SIZE_IS)
    {
        if (param->is_sub()) {
            ostringstream s;
            s << "primary param is set to sub before ("
              << param->context_name() << ")";
            throw runtime_error(s.str());
        }
    }

    // interface_is, for [iid_is]
    if (tag == TD_INTERFACE_IS_TYPE) {
        param_decl* iid_is = get_sub_param(param, td->argnum);
        check_iid_is_param(iid_is);

        // XXX only if reference param is type [in] and uuid_ref
        if (iid_is->is_in() && iid_is->type.tag == type_decl::tag_uuid_ref) {
            iid_is->add_primary(param);
            iid_is->flags |= param_decl::flag_iid_is;

            param->flags |= param_decl::flag_primary;
            param->iid_is = iid_is;

            // Checks if other qi referencing this
            if (iid_is->primary_count > 1)
                param->qi_index = iid_is->primary()->qi_index;
            else
                param->qi_index = param->method->qi_count++;

        } else {
            param->type.tag = type_decl::tag_interface_qi_out;
        }
        return;
    }

    // array, pstring_size_is, for [size_is] and [length_is]
    if (tag == TD_ARRAY ||
        tag == TD_PSTRING_SIZE_IS || tag == TD_PWSTRING_SIZE_IS)
    {
        if (td->argnum == td->argnum2) {
            param_decl* size_is = get_sub_param(param, td->argnum);
            check_size_is_param(size_is);

            size_is->add_primary(param);
            size_is->flags |= (param_decl::flag_size_is | param_decl::flag_length_is);

            param->flags |= param_decl::flag_primary;
            param->size_is = param->length_is = size_is;

        } else {
            param_decl* size_is = get_sub_param(param, td->argnum);
            param_decl* length_is = get_sub_param(param, td->argnum2);
            check_size_is_param(size_is);
            check_size_is_param(length_is);
            BOOST_ASSERT(size_is != length_is);

            // TODO exception safety
            size_is->add_primary(param);
            length_is->add_primary(param);
            size_is->flags |= param_decl::flag_size_is;
            length_is->flags |= param_decl::flag_length_is;

            param->flags |= param_decl::flag_primary;
            param->size_is = size_is;
            param->length_is = length_is;
        }
        return;
    }
}

param_decl* input_xpt::get_sub_param(param_decl* param, size_t n) const
{
    method_decl* method = param->method;
    param_decl* ret = 0;

    if (n < method->params.size()) {
        ret = method->params[n];
    } else if (n == method->params.size()) {
        ret = method->result;
    } else {
        ostringstream s;
        s << "wrong sub param (" << n << ":" << method->params.size()
          << ") in (" << param->context_name() << ")";
        throw runtime_error(s.str());
    }

    if (ret == param) {
        ostringstream s;
        s << "param referenced self in (" << param->context_name() << ")";
        throw runtime_error(s.str());
    }

    return ret;
}

interface_decl* input_xpt::root_interface() const
{
    BOOST_ASSERT(!module()->interfaces.empty());
    return module()->interfaces[0];
}

interface_decl* input_xpt::xpt_interface(size_t index) const
{
    if (index >= itf_map_.size())
        index = 0;
    return itf_map_[index];
}

bool input_xpt::is_interface_need_size_t(interface_decl* itf)
{
    return (itf->flags & flag_check_size_t) != 0;
}

void input_xpt::check_interface(XPTInterfaceDirectoryEntry* ide)
{
    if (!ide->name || !(*ide->name))
        throw runtime_error("interface name is null");

    // XXX Don't allow interface name of 'interface'/*, 'base'*/...
    if (strcmp(ide->name, N_interface) == 0)
        throw runtime_error("interface name can not be '" N_interface "'");
#if 0
    if (strcmp(ide->name, N_base) == 0)
        throw runtime_error("interface name can not be '" N_interface "'");
#endif
}

bool input_xpt::is_root_interface(XPTInterfaceDirectoryEntry* ide)
{
    // For ::nsISupports
    return strcmp(ide->name, N_nsISupports) == 0 &&
        (!ide->name_space || !(*ide->name_space));
}

void input_xpt::check_constant(interface_decl* itf, XPTConstDescriptor* cd)
{
    if (!cd->name || !(*cd->name)) {
        throw runtime_error(
                "interface " + itf->qname + " has null constant name");
    }

    check_type(itf, &cd->type);

    int tag = XPT_TDP_TAG(cd->type.prefix);
    switch (tag) {
        case TD_INT16:
        case TD_INT32:
        case TD_UINT16:
        case TD_UINT32:
            break;

        default:
        {
            // XXX const declaration must be of type short or long
            ostringstream s;
            s << "const declaration must be of type short or long ("
              << tag << ") in (" << itf->qname << "." << cd->name;
            throw runtime_error(s.str());
        }
    }
}

void input_xpt::check_method(interface_decl* itf, XPTMethodDescriptor* md)
{
    if (!md->name || !(*md->name))
        throw runtime_error("interface " + itf->qname + " has null method name");
}

void input_xpt::check_param(interface_decl* itf, XPTParamDescriptor* pd)
{
    // Nothing
}

void input_xpt::check_type(interface_decl* itf, XPTTypeDescriptor* td)
{
    int tag = XPT_TDP_TAG(td->prefix);
    if (tag < TD_INT8 || tag > TD_ASTRING) {
        ostringstream s;
        s << "interface " << itf->qname << " has invalid type (" << tag << ")";
        throw runtime_error(s.str());
    }

    if (tag == TD_ARRAY) {
        XPTInterfaceDescriptor* desc =
                static_cast<XPTInterfaceDescriptor*>(itf->user_data);
        check_type(itf, &desc->additional_types[td->type.additional_type]);
    }
}

void input_xpt::check_iid_is_param(param_decl* param)
{
    if (param->is_primary() ||
        param->type.is_array ||
        !param->type.is_uuid())
    {
        ostringstream s;
        s << "iid_is param is not uuid (" << param->context_name() << ")";
        throw runtime_error(s.str());
    }
}

void input_xpt::check_size_is_param(param_decl* param)
{
    if (param->is_primary() ||
        param->type.is_array ||
        param->type.tag != type_decl::tag_uint32)
    {
        ostringstream s;
        s << "size_is|length_is param is not uint32 ("
          << param->context_name() << ")";
        throw runtime_error(s.str());
    }
}

} // namespace xpgen
